use std::ops::{Deref, DerefMut};

use mark_dirty::DirtyMark;
use wgpu::util::DeviceExt;

use crate::{buffer::DeviceTexture2D, AsBytes, Context, Pixmap};

use super::Attribute;

pub struct Uniform<T: AsBytes> {
    data: DirtyMark<T>,
    buffer: wgpu::Buffer,
}

impl<T: AsBytes> Uniform<T> {
    pub fn new(ctx: &Context, data: T) -> Self {
        let buffer = ctx
            .device
            .create_buffer_init(&wgpu::util::BufferInitDescriptor {
                label: None,
                contents: data.as_bytes(),
                usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
            });

        Self {
            data: DirtyMark::new(data),
            buffer,
        }
    }

    pub fn set_data(&mut self, data: T) {
        *self.data.inner_mut() = data;
    }
}

impl<T: AsBytes + 'static> Attribute for Uniform<T> {
    fn ty(&self) -> wgpu::BindingType {
        wgpu::BindingType::Buffer {
            ty: wgpu::BufferBindingType::Uniform,
            has_dynamic_offset: false,
            min_binding_size: None,
        }
    }

    fn res(&self) -> wgpu::BindingResource {
        self.buffer.as_entire_binding()
    }

    fn prepare(&mut self, ctx: &crate::Context) {
        if self.data.is_dirty() {
            ctx.queue
                .write_buffer(&self.buffer, 0, self.data.inner().as_bytes());
            self.data.unmark_dirty();
        }
    }
}

impl<T: AsBytes> Deref for Uniform<T> {
    type Target = T;

    fn deref(&self) -> &Self::Target {
        &self.data
    }
}

impl<T: AsBytes> DerefMut for Uniform<T> {
    fn deref_mut(&mut self) -> &mut Self::Target {
        &mut self.data
    }
}

impl Attribute for wgpu::Sampler {
    fn ty(&self) -> wgpu::BindingType {
        wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering)
    }

    fn res(&self) -> wgpu::BindingResource {
        wgpu::BindingResource::Sampler(self)
    }

    fn prepare(&mut self, _: &crate::Context) {}
}

pub struct Texture2D {
    pixmap: DirtyMark<Pixmap>,
    texture: DeviceTexture2D,
}

impl Texture2D {
    pub fn new(ctx: &Context, pixmap: Pixmap, format: wgpu::TextureFormat) -> Self {
        let texture = DeviceTexture2D::with_pixmap(ctx, &pixmap, format);

        Self {
            pixmap: DirtyMark::new(pixmap),
            texture,
        }
    }

    pub fn recreate(&mut self, ctx: &Context) {
        self.texture.resize(ctx, self.pixmap.width(), self.pixmap.height())
    }

    pub fn format(&self) -> wgpu::TextureFormat {
        self.texture.format()
    }
}

impl Attribute for Texture2D {
    fn ty(&self) -> wgpu::BindingType {
        self.texture.ty()
    }

    fn res(&self) -> wgpu::BindingResource {
        self.texture.res()
    }

    fn prepare(&mut self, ctx: &crate::Context) {
        if self.pixmap.is_dirty() {
            if self.pixmap.width() != self.texture.width()
                || self.pixmap.height() != self.texture.height()
            {
                self.recreate(ctx);
            }

            self.texture.write_pixels(ctx, 0, 0, self.pixmap.width(), self.pixmap.height(), self.pixmap.pixels.as_slice().as_bytes(), self.pixmap.data_layout());
            self.pixmap.unmark_dirty();
        }
    }
}
